-- DungeonM
-- Create by panyl
-- 地牢

module("DungeonM", package.seeall);

-- 所有的元素信息
local dungeonClass = {};
local elements = {};
local elementsToDungeon = {};

-- 预加载资源配置表
local asyncResTable = {};
local asyncResByDungeon = {};

-- 所有的规则处理子模块
local rules = {};

-- 地牢数据
local dungeonContainer = {};

-- 当前地牢数据
local curDungeon = nil;

-- 所有的操作缓存
local actionCache = {};
local delayAction = {};

-- 当前状态
local state = DUNGEON_LEAVE;

-- 围格列表
local aroundGridTable = {};

-- 临格列表
local adjoinGridTable = {};

-- 地牢类型
local dungeonType = DUNGEON_TYPE_NORMAL;

-- 载入配置表
local _init = false;
local loadCsv = function()
    if _init then return end
    _init = true;

    -- 转换下dungeon信息表
    dungeonClass = {};
    local dungeon_CSV = get_dungeon_CSV();
    for i = 1, #dungeon_CSV do
        dungeonClass[dungeon_CSV[i].class] = dungeon_CSV[i];
    end
    dungeon_CSV = {};

    -- 转换下dungeon_element信息表
    elements = {};
    elementsToDungeon = {};
    local dungeon_element_CSV = get_dungeon_element_CSV();
    for i = 1, #dungeon_element_CSV do
        local id = dungeon_element_CSV[i].id;
        elements[id] = dungeon_element_CSV[i];

        -- 按照地牢存放元素
        local dungeon = dungeon_element_CSV[i].dungeon;
        local class = dungeon_element_CSV[i].class;
        if not elementsToDungeon[dungeon] then
            elementsToDungeon[dungeon] = {};
        end

        if not elementsToDungeon[dungeon][class] then
            elementsToDungeon[dungeon][class] = {};
        end
        table.insert(elementsToDungeon[dungeon][class], dungeon_element_CSV[i]);
    end
    dungeon_element_CSV = {};

    -- 载入预加载资源
    asyncResByDungeon = {};
    local dungeon_async_res_CSV = get_dungeon_async_res_CSV();
    for i = 1, #dungeon_async_res_CSV do
        local row = dungeon_async_res_CSV[i];
        local id  = row.id;
        local dungeon = row.dungeon_id;

        asyncResTable[id] = row;

        -- 按照迷宫编号存放
        if not asyncResByDungeon[dungeon] then
            asyncResByDungeon[dungeon] = {};
        end
        table.insert(asyncResByDungeon[dungeon], id);
    end

    local total = DUNGEON_WIDTH * DUNGEON_HEIGHT;

    -- 初始化围格列表
    for pos = 1, total do
        local aroundList = {};
        for p = 1, total do
            if isAround(pos, p) then
                table.insert(aroundList, p);
            end
        end
        aroundGridTable[pos] = aroundList;
    end

    -- 初始化临格列表
    for pos = 1, total do
        local adjoinList = {};
        for p = 1, total do
            if isAdjoin(pos, p) then
                table.insert(adjoinList, p);
            end
        end
        adjoinGridTable[pos] = adjoinList;
    end
end

-- 模块初始化
function init()
    -- 初始化
    state = DUNGEON_LEAVE;
    loadCsv();

    -- 载入所有的规则处理子模块
    rules = LOAD_PATH2("game/logic/module/dungeon");

    EventMgr.removeAll("DungeonM");
    EventMgr.register("DungeonM", event.LOGIN_OK, function()
        -- 状态初始化为离开
        switchState(DUNGEON_LEAVE);
        actionCache = {};
    end);
end

-- 检索格子类信息
function query(class, path)
    local m = dungeonClass[class]

    if  not m or
        not path or path == "" then
        return m
    end

    return m[path]
end

-- 检索元素的配置信息
function queryElement(id, path)
    local m = elements[id];

    if not m or
       not path or path == "" then
        return m;
    end

    return m[path];
end

-- 检索预加载资源配置
function queryAsyncRes(id, path)
    local m = asyncResTable[id];

    if not m or not path or path == "" then
        return m;
    end

    return m[path];
end

-- 根据迷宫编号获取预加载资源
function getAsyncResByDungeon(dungeonId)
    local list = asyncResByDungeon[dungeonId];
    if type(list) ~= "table" or #list == 0 then
        return {};
    end

    local ret = {};
    for _, id in pairs(list) do
        local path = queryAsyncRes(id, "path");
        table.insert(ret, path);
    end
    return ret;
end

-- 根据迷宫编号获取预加载的.png资源和.csb资源
-- 满足表中配置的加载条件公式：load_condition才会加载
-- @param dungeonId 地牢Id
function getAsyncDiffResByDungeon(dungeonId)
    local list = asyncResByDungeon[dungeonId];
    if type(list) ~= "table" or #list == 0 then
        return {};
    end

    local resRet = {};
    local csbRet = {};
    for _, id in pairs(list) do
        local path = queryAsyncRes(id, "path");

        -- 根据配置的公式判断是否满足加载条件
        -- TODO: 目前只支持BOSS层的判断，如果有多个加载条件的话，在接口中传参数
        local canLoad = false;
        local formulaName = queryAsyncRes(id, "load_condition");
        if formulaName ~= "" then
            canLoad = FormulaM.invoke(formulaName);
        end

        -- 如果满足加载条件，那么加入列表
        if canLoad then
            if getFileExtension(path) == "csb" then
                table.insert(csbRet, path);
            else
                table.insert(resRet, path);
            end
        end

    end
    return resRet, csbRet;
end

-- 获取元素
function getClassByType(type, rule)
    local arr = table.keys(dungeonClass);

    table.sort(arr);

    for _, class in pairs(arr) do
        local info = dungeonClass[class];
        if info.type == type and
            (not rule or info.rule == rule) then
            return class;
        end
    end

    return 0;
end

-- 获取子模块
function getModule(class)
    local row = dungeonClass[class];
    local ruleName = string.trim(row.rule);
    local rule = rules[ruleName];
    if rule == "TBL" then
        rules[ruleName] = LOAD_RUN("game/logic/module/dungeon/" .. ruleName);
        return rules[ruleName];
    end

    return rule;
end

-- 根据地牢id获取元素列表
function getElementsByDungeon(dungeonId)
    return elementsToDungeon[dungeonId];
end

-- 当前层
function currentLayer()
    if not dungeonContainer then
        return 0
    end

    -- 当前层
    return dungeonContainer.currLayer;
end

-- 获取当前层地牢格子
function getGrids()
    local layer = currentLayer();
    if not layer then
        return
    end

    return dungeonContainer.combats[layer];
end

-- 获取格子
function getGridByPos(pos)
    -- 如果不在迷宫内了
    if not curDungeon then
        return {};
    end

    return curDungeon[pos];
end

-- 当前地牢格子
function getCurrentDungeon()
    return curDungeon;
end

-- 当前地牢id
function getDungeonId()
    if not dungeonContainer then
        return 0;
    end

    if type(dungeonContainer.dungeonId) == "number" then
        return dungeonContainer.dungeonId;
    end

    if ME.isInGame then
        local dungeonId = ME.user.dbase:query("current_dungeon");
        return dungeonId or 0;
    end

    return 0;
end

-- 获取隐形障碍的class
function getInvisibleBlockClass()
    local arr = table.keys(dungeonClass);

    for _, class in pairs(arr) do
        local info = dungeonClass[class];
        local dbase = info.dbase or {};
        if info.type == GRID_TYPE_BLOCK and
            dbase.invisible == 1 then
            return class;
        end
    end

    return 0;
end

-- 设置地牢数据
function setDungeon(dungeon)
    dungeonContainer = dungeon;
end

-- 迷宫数据
function notifyDungeonData(data, context, restart)
    Profiler.funcBegin("notifyDungeonData");

    -- 地牢数据更新
    local layer = data.layer;
    local dungeon = {
        identify  = data.identify,              -- 识别号
        dungeonId = data.dungeon_id,            -- 地牢编号
        currLayer = layer,                      -- 当前处于第几层
        layer     = layer,                      -- 为了兼容服务器
        rand_seed = data.rand_seed,             -- 随机种子
        combats   = {},                         -- 各层元素信息
        keyPos    = {},                         -- 各层钥匙位置
    };

    -- 对每一层的地牢数据解析出来
    for offset, t in pairs(data.combats) do
        -- 某层的格子元素数据，应该是{ { class1 : element1, }, ... {class30 : element30 }, keyPos, }这样的格式
        local elements = t;

        -- 最后一个是钥匙
        dungeon.keyPos[layer + offset - 1] = elements[#elements] + 1;
        elements[#elements] = nil;

        dungeon.combats[layer + offset - 1] = elements;

        local currLayer = layer + offset - 1;
        local cacheStat = (currLayer ~= layer);
        local pauseContinue = false;
        if isPause() and currLayer == layer then
            pauseContinue = true;
        end

        -- 统计格子
        if not pauseContinue then
            DungeonStatM.doLayerStat(currLayer, elements);
        end
    end

    -- 如果是新开始的，需要把宠物职业信息清除
    if restart == 1 then
        ME.user.dbase:setTemp("restart", 1);
        CareerM.clearPetCareer();
    end

    ME.user.dbase:set("current_dungeon", data.dungeon_id);

    -- 记录最高层数
    local max_dungeon_layer = ME.user.dbase:query("max_dungeon_layer", 0);
    max_dungeon_layer = math.max(max_dungeon_layer, layer);
    ME.user.dbase:set("max_dungeon_layer", max_dungeon_layer);

    ME.user.dbase:set("dungeon_context", context);

    -- 怪物全清标记
    if context and context["cleared_layer"] then
        ME.user.dbase:setTemp("monster_cleared", context["cleared_layer"]);
    end

    -- 进迷宫之前先刷新下属性
    PropM.refresh(ME.user);

    -- 更新地牢数据
    updateDungeon(dungeon, context, true, restart);

    Profiler.funcEnd("notifyDungeonData");
end

-- 更新地牢数据
function updateDungeon(dungeon, context, startFloor, restart)
    dungeonContainer = dungeon;

    startDungeon(context, startFloor, restart);
end

-- 更新地牢数据,只更新数据
function updateDungeonInfo(dungeon)
    dungeonContainer = dungeon;
end

-- 获取地牢数据
function getDungeonContainer()
    return dungeonContainer;
end

-- 是否在地牢内
function isInDungeon()
    if state ~= DUNGEON_LEAVE then
        return true;
    end

    if ME.isInGame then
        local dungeonId = ME.user.dbase:query("current_dungeon");
        return type(dungeonId) == "number" and dungeonId > 0;
    end

    return false;
end

--------------------
-- 暂离地牢
-- @param doSync  是否同步
function pauseDungeon(doSync)
    Profiler.funcBegin("pauseDungeon");

    if doSync then
        -- 如果需要同步，先增加同步缓存操作
        addAction({ ["cmd"] = "pause_dungeon", });
    end

    -- 收集地牢状态并打上标记
    local round = CombatM.getRound();
    local prop  = table.copy(ME.user.dbase:queryTemp("prop"));
    local context = {};

    -- 去掉skill、equip、status、lictor、workshop、relic_func这类固定的属性
    prop["skill"] = nil;
    prop["equip"] = nil;
    prop["combat_status"] = nil;
    prop["lictor"] = nil;
    prop["workshop"] = nil;
    prop["relic_func"] = nil;
    prop["summon"] = nil;

    -- 将玩家 temp 的 summon_list 清除
    ME.user.dbase:deleteTemp("summon_list");

    for pos = 1, GRID_SIZE do
        context[pos] = collectContext(pos);
    end

    -- 记录下来
    ME.user.dbase:set("dungeon_context", {
        round   = round,
        prop    = prop,
        context = context,
        combat_status = ME.user.dbase:query("combat_status", {}),
        summon  = SummonM.collect_summon_data(),
    });

    -- 切换到暂停阶段
    switchState(DUNGEON_PAUSING);

    -- 抛出事件
    local dungeonId = getDungeonId();
    local layer     = currentLayer();
    EventMgr.fire(event.DUNGEON_PAUSED, { dungeonId, layer, });

    if doSync then
        -- 同步
        sync();

        -- 强制请求一次炼金消息
        Operation.cmd_check_workshop();
        -- 重设StartTime
        local nowTime = TimeM.getCurrentTime();
        AlchemyWorkshopM.setStartTime(nowTime + 1);
    end

    Profiler.funcEnd("pauseDungeon");
    return true, doSync;
end

-- 保存地牢进度
function saveDungeon()
    -- 添加操作缓存
    addAction({ ["cmd"] = "save_dungeon", });

    -- 同步
    sync();

    -- 状态切换为游戏中
    switchState(DUNGEON_GAMING);
    return true, true;
end

-- 保存地牢引导进度
function saveGuideProgress(progress)
    ME.user.dbase:set("dungeon_guide_progress", progress);
    addAction({ ["cmd"] = "save_guide_progress", ["data"] = progress });
    return true, true;
end

-- 退出地牢
function leaveDungeon()
    -- 如果不是在迷宫中
    if not isInDungeon() then
        return false;
    end

    -- 先从副本中离开
    if DungeonInstanceM.isInInstance() and
        not DungeonActionM.go("leave_instance") then
        return false;
    end

    -- 地牢内执行的离开操作需要在同步消息中处理
    if getState() == DUNGEON_GAMING then
        local isDead = iif(ME.user:isDead(), 1, 0);

        -- 增加离开操作
        addAction({ ["cmd"] = "leave_dungeon", ["data"] = isDead, });

        -- 同步
        sync();
    elseif getState() == DUNGEON_LEAVE then
        -- 否则直接发消息
        Communicate.send("CMD_LEAVE_DUNGEON", {});
    end

    -- 强制请求一次炼金消息
    Operation.cmd_check_workshop();
    -- 重设StartTime
    local nowTime = TimeM.getCurrentTime();
    AlchemyWorkshopM.setStartTime(nowTime + 1);

    local dungeonId = getDungeonId();
    local layer     = currentLayer();

    if not dungeonId or not layer then
        dungeonId = ME.user.dbase:query("currentDungeon", 0);

        local data = ME.user.dbase:query("dungeon_progress", {})[dungeonId] or {};
        layer = data.curr or 0;
    end

    -- 更新最新的星级
    local star = FormulaM.invoke("CALC_DUNGEON_STAR", dungeonId, layer);
    local dungeonProgress = ME.user.dbase:query("dungeon_progress", {});
    dungeonProgress[dungeonId]["curr"] = 0;
    dungeonProgress[dungeonId]["star"] = star;
    ME.user.dbase:set("dungeon_progress", dungeonProgress);

    trace("DungeonM", "本次地牢探索到%d层，地牢星级为：%d", layer, star);

    -- 注意！其他清空数据操作请到DungeonServiceM.whenLeaveDungeon添加

    catch(function()
        -- 切换状态
        switchState(DUNGEON_LEAVE);

        -- 战斗统计奖励
        CombatStatM.combatEndBonus(dungeonId, layer);

        -- 清除状态
        CombatStatusM.clearAllStatus(ME.user);

        -- 事件
        EventMgr.fire(event.LEAVE_DUNGEON);

        -- 抛出结算完成事件
        EventMgr.fire(event.COMBAT_STAT_FINISHED, { ["dungeon_id"] = dungeonId, ["layer"] = layer, });
    end);

    -- 清空数据
    dungeonContainer = {};
    curDungeon = nil;
    return true;
end

-- 切换状态
function switchState(s)
    state = s;
    trace("DungeonM", "状态切换为：%d", s);
end

-- 获取当前状态
function getState()
    return state;
end

-- 下一层，这里不加判断了
function nextFloor(noAction)
    -- 正在自动打开格子
    if DungeonServiceM.getAutoOpeningFlag() then
        return false;
    end

    -- 是否门已经打开了
    local door = CombatM.filterByClass(getCurrentDungeon(), GRID_TYPE_DOOR)[1];
    if not noAction and not door:isOpened() then
        return false;
    end

    -- 公式判断一下
    local check = FormulaM.invoke("DO_NEXT_FLOOR_CHECK");
    if true ~= check then
        -- 失败了
        return check, true;
    end

    -- 增加进入下层行为
    if not noAction then
        addAction({ ["cmd"] = "next_floor", });
    end

    -- 如果此时不是DUNGEON_GAMING状态，不应该走下面这几个操作
    if getState() == DUNGEON_GAMING then
        -- 状态切换为载入中（要在上面addAction之后再切换，否则会addAction失败)
        switchState(DUNGEON_LOADING);

        -- 进入下一层的处理
        DungeonServiceM.whenPassFloor();

        -- 所有缓存行为发往服务器
        sync();
    end

    -- 数据指向下一层
    local currLayer = dungeonContainer.currLayer;
    local nextLayer = currLayer + 1;
    dungeonContainer.combats[currLayer] = nil;
    dungeonContainer.keyPos[currLayer]  = nil;

    -- 如果没有数据了
    local maxLayer = FormulaM.invoke("CALC_DUNGEON_LAYER_LIMIT", getDungeonId());
    if not dungeonContainer.combats[nextLayer] and
        maxLayer >= nextLayer then
        if TestFightM.isInTest() then
            -- TODO暂时这么处理一下
            TestFightM.noDungeonInfo();
        end
        doAlert(getLocStr("bad_network_tip"));

        -- 切入等待下一层数据状态
        switchState(DUNGEON_WAIT_NEXT);

        -- 抛出等待中界面，然后再去服务器获取一把数据
        EventMgr.fire(event.WAIT_LOADING);

        -- 这里不能走cmd_start_dungeon流程，不然服务器可能会洗牌一次，造成两边不一致
        local authId = getUniqueNumber();
        local v = {["dungeon_id"] = getDungeonId(), ["layer"] = nextLayer, ["auth_id"] = authId, };
        Communicate.send("CMD_FETCH_DUNGEON", v);

        -- 重发队列队列
        DungeonServiceM.addWaitSync("CMD_FETCH_DUNGEON", v);

        return false, true;
    end

    -- 层数+
    dungeonContainer.currLayer = nextLayer;

    -- 更新最高层数
    local max_dungeon_layer = ME.user.dbase:query("max_dungeon_layer", 0);
    max_dungeon_layer = math.max(max_dungeon_layer, nextLayer);
    ME.user.dbase:set("max_dungeon_layer", max_dungeon_layer);

    local dungeonId = getDungeonId();

    -- 抛出事件
    if maxLayer >= nextLayer then
        -- 还未达到最高层
        EventMgr.fire(event.NEXT_FLOOR);
    else
        -- 记录之前是否通关过
        if dungeonId ~= DungeonAreaM.getParentId(dungeonId) then
            -- 只记录小关卡的
            ME.user.dbase:setTemp("isDungeonPassed", {pass = DungeonAreaM.isDungeonPassed(ME.user, dungeonId), id = dungeonId});
        end

        DungeonAreaM.setDungeonPassed(ME.user, dungeonId);

        -- 达到了最高才
        EventMgr.fire(event.DUNGEON_PASS, { ["dungeonId"] = dungeonId, });

        if DungeonInstanceM.isInInstance() and
            not DungeonActionM.go("leave_instance") then
            return;
        end

        DungeonActionM.go("leave_dungeon");
    end

    -- 删除清怪标记
    ME.user.dbase:deleteTemp("monster_cleared");

    -- 开始进入下一层
    EventMgr.fire(event.START_NEXT_FLOOR);

    -- 开始
    if not isVerifyClient() and not StatusM.check(ME.user, "PLAYING_RECORD") then
        -- 验证客户端不需要开始下一层
        startDungeon(nil, true);
    end

    -- action已经添加上去了
    return true, true;
end

-- 是否是最高层
function isMaxLayer()
    if currentLayer() < FormulaM.invoke("CALC_DUNGEON_LAYER_LIMIT", getDungeonId()) then
        return false;
    else
        return true;
    end
end

-- 开始新一层
function startDungeon(context, startFloor, restart)
    -- 如果已经是游戏状态了
    if state == DUNGEON_GAMING and
       not DungeonM.isPause() then
        trace("DungeonM", "正在游戏中。")
        return;
    end

    -- 没有地牢数据
    if not dungeonContainer then
        return;
    end

    local layer = currentLayer();

    if not layer then
        return;
    end

    -- 取不到当前地牢数据
    if not dungeonContainer.combats[layer] then
        -- 客户端会缓存5层数据，如果断层了说明有某个消息没有收到，或者根本就没网络现在
        -- 这里只要等待就行了
        trace("DungeonM", "取不到当前地牢数据，执行等待。");
        return;
    end

    -- 在等待进入地牢的消息
    if DungeonServiceM.getStartTime() then
        -- 在等待进入地牢消息，服务端会通知重新排列当前层数据
        -- 如果不等待会导致重复生成（服务端/客户端随机数游标不匹配）
        trace("DungeonM", "等待服务器回应进入地牢数据，执行等待。");
        return;
    end

    -- 收集下数据
    if startFloor then
        DungeonLogM.collectEntireUser();
    end

    local pet = ME.user:getActivePet();
    local msg = string.format("携带%d宠物开始第(%d/%d)层地牢。", pet.classId, getDungeonId(), layer);
    DungeonLogM.addLog(msg);
    DungeonDebugM.clear();

    -- TODO: 清除属性？
    local prop = ME.user.dbase:queryTemp("prop");
    if prop then
        prop.skill_prop = nil;
        prop.shield = nil;
        prop.debuff = nil;
        prop.buff = nil;
        prop.special = nil;
        prop.mixed = nil;
        prop.magic_stone = nil;
        prop.tarot_fool = nil;
        prop.tarot_prieste = nil;
        prop.summon = nil;
        prop.assist = nil;
        prop.assist_summon = nil;
        ME.user.dbase:triggerField("prop");
    end

    -- 将玩家 temp 的 summon_list 清除
    ME.user.dbase:deleteTemp("summon_list");

    -- 重整一下属性
    PropM.arrangeProps(ME.user);

    -- 当前地牢
    curDungeon = {};

    -- 当前层的所有格子元素
    local data = getGrids();
    local grids = sortGrids(data);
    local bossBoxBackup = ME.user.dbase:query("boss_box_backup", {});
    local boxIndex = 1;

    for _, t in pairs(grids) do
        local pos     = t.pos;
        local class   = t.class;
        local element = t.element;
        local state   = t.state;

        -- 转化为坐标
        local coordinate = convertToCoor(pos);
        local x = coordinate.x;      -- 横坐标
        local y = coordinate.y;      -- 纵坐标

        local attrib = {};
        local dbase = {};

        -- TODO:空格也需要有类别
        local category = 0;

        -- 没属性的（空格、障碍这些）
        local nothing = true;
        local args = queryElement(element, "args") or {};
        -- 如果是金祭坛
        if FormulaM.invoke("IS_INVALID_DUNGEON_GRID", class, element) then
            -- 小关卡的boss宝箱有可能空出位置
            -- 因为提前抽取了“试剂配方”等必须出现的物品，然后前面几层又被拾取掉了
            -- 就会导致被强制刷成空格
            -- 这时候需要启用备用宝箱
            local classDbase = query(class, "dbase") or {};
            local dungeonId = getDungeonId();
            if DungeonBossM.isBossLayer(dungeonId, layer) and
                DungeonAreaM.query(dungeonId, "type") == "child" and
                DungeonAreaM.query(dungeonId, "max_floor") == layer and
                classDbase["top_appear"] and boxIndex <= #bossBoxBackup then
                element = bossBoxBackup[boxIndex];
                class = queryElement(element, "class");

                boxIndex = boxIndex + 1;

                nothing = false;
            else
                -- 无效了，刷成空格
                element = 0;
                class = getClassByType(GRID_TYPE_EMPTY);
                nothing = true;
            end
        elseif element > 0 then
            -- 其他的都有属性
            nothing = false;
        end

        -- 有属性的（像空格、障碍这些是没有属性的）
        if not nothing then
            class = queryElement(element, "class");

            -- 格子的属性
            dbase  = queryElement(element, "dbase");

            DungeonLogM.addLog("[DungeonM]抽取地牢元素：" .. tostring(element) .. " pos：" .. pos);

            attrib = FormulaM.invoke("CALC_DUNGEON_ELEMENT_ATTRIB", getDungeonId(), element, layer, pos);

            -- 如果奖励配置的是公式，需要调用公式重新计算
            local prefix = "formula_";

            -- 如果配置了公式，则调用公式计算
            if type(attrib) == "table" and type(attrib.bonus) == "string" and
                string.startWith(attrib.bonus, "formula_") then
                local formulaName = string.sub(attrib.bonus, string.len(prefix) + 1);
                attrib.bonus = FormulaM.invoke(formulaName, ME.user, attrib.bonusArg);
            end

            -- 物品类别
            category = queryElement(element, "category");
        end

        local entir = table.deepcopy(table.add(dbase or {}, attrib or {}));

        -- 如果是钥匙
        if dungeonContainer.keyPos[layer] == pos then
            -- 把掉落干掉
            entir.bonus = nil

            entir.has_key = 1;
        end

        entir.layer = layer;
        entir.pos = pos;

        -- 物品类别
        entir.category = category;

        entir.element = element;

        -- 创建一个格子
        curDungeon[pos] = Grid.new(class, x, y, GRID_STATE_COVER, entir);

        -- 统计格子
        if sizeof(context) <= 0 then
            DungeonStatM.currStat(class, 1, element);
        end
    end

    -- 状态切换为游戏中
    switchState(DUNGEON_GAMING);

    if startFloor then
        DungeonActionM.go("start_dungeon");
    end

    -- 还原格子
    for _, grid in pairs(curDungeon) do
        restoreGrid(grid, context);
    end

    -- 还原钥匙格子
    restoreKeyGrid();

    -- 因为魔藤会影响到玩家属性，所以这里需要先生成魔藤
    BineM.startFloor();

    if context and #table.keys(context) > 0 then
        -- 还原下召唤兽数据
        SummonM.restore_summon_data(context["summon"] or {});

        -- TODO 还原属性
        if context.prop then
            local prop = context.prop;
            ME.user.dbase:setTemp("prop", prop);

            -- 状态
            local combatStatus = context.combat_status or {};
            CombatStatusM.resetAllStatus(ME.user, combatStatus);
            -- CombatStatusM.updateStatus(ME.user, combatStatus);

            -- 再刷新一次，如果下面这个refresh删除掉的话记得在这里做PropM.arrangeProps
            -- 这里必须要刷新一下，因为上面的prop不全，没有包含skill、equip、status、lictor、workshop、relic_func这类固定的属性
            PropM.refresh(ME.user);
        end

        -- 设置回合数
        CombatM.setRound(layer, context.round);

        -- 抛出事件
        EventMgr.fire(event.CONTINUE_DUNGEON, layer);
    else
        -- 还原下召唤兽数据
        SummonM.restore_summon_data({});

        -- 做一次随机数偏移
        RandomFactoryM.shiftRandomCursor();

        -- 设置回合数
        CombatM.setRound(layer, 1);

        -- 清除状态
        CombatStatusM.clearAllStatus(ME.user, true);

        -- 触发一些属性，需要在START_DUNGEON之前，因为START_DUNGEON事件回调会影响到一些触发
        DungeonServiceM.enterTrigger();

        -- 地牢准备完毕
        EventMgr.fire(event.DUNGEON_PREPARE_OVER);

        -- 显示公开的格子
        DungeonServiceM.showPublicGrid();

        -- 抛出事件
        EventMgr.fire(event.START_DUNGEON, layer);
    end

    -- 刷新光环属性
    AssistMonsterM.updateAssistData();
    AssistMonsterM.updateGridAssistData();
    AssistMonsterM.updateSummonAssist();

    if NewbieDungeonM.isInNewbieDungeon() then
        -- 抛出进入指引迷宫的事件
        EventMgr.fire(event.ENTER_NEWBIE_DUNGEON, layer);
    end

    if ME.user.dbase:query("dungeon_context") then
        ME.user.dbase:delete("dungeon_context");
    end

    -- 记录下当前的基础属性和附加属性，装备、称号等等
    DungeonLogM.recordCurrentState();
    EventMgr.fire(event.DUNGEON_AREADY, {["restart"] = restart});

    whenDungeonAready();
end

-- 迷宫准备完毕回调
function whenDungeonAready()

    -- 触发侦测隐藏物件的属性
    local props = PropM.fetchProps(ME.user, "show_hide_item");
    for _, prop in ipairs(props) do
        PropM.trigger(ME.user, prop[1], prop[2]);
    end

    -- 开始迷宫回调
    DungeonServiceM.handleStartDungeon();
end

-- 同步所有操作
function sync()
    if isVerifyClient() then
        -- 验证客户端不需要同步
        return;
    end

    -- 如果有缓存的操作
    Operation.cmd_dungeon_action(dungeonContainer.identify, actionCache);

    -- 清空缓存
    actionCache = {};

    switchState(DUNGEON_LOADING);

    -- 检查下网络
    LoginM.checkNetwork();
end

-- 清空同步操作缓存
function clearActionCache()
    -- 清空缓存
    actionCache = {};
end

-- 是否可以打开格子
function canOpenGrid(pos)
    Profiler.funcBegin("canOpenGrid");

    -- 格子
    local grid = getCurrentDungeon()[pos];

    -- 1. 已经翻开过了
    if grid.state ~= GRID_STATE_COVER then
        return GRID_OPEN_DONE;
    end

    -- 2. 如果是门，则不能直接翻开
    if grid.type == GRID_TYPE_DOOR then
        return GRID_OPEN_DISABLE;
    end

    -- 3. 周围是否有怪物
    local grids = getCurrentDungeon()
    for _, p in pairs(getAroundGrids(pos)) do
        local g = grids[p];

        if g.state == GRID_STATE_OPEN and
            g:isMonster() and
            FormulaM.invoke("CAN_MONSTER_DEFENSE_GRID", g) then
            -- 周围有怪物
            return GRID_OPEN_LOCK;
        end
    end

    -- 4. 必须有连通的临格
    local ok = GRID_OPEN_DISABLE;
    for _, p in pairs(getAdjoinGrids(pos)) do
        local g = grids[p];

        local block = (g.type == GRID_TYPE_BLOCK);
        local dbase = query(g.class, "dbase") or {};
        if 1 == dbase["invisible"] then
            block = false;
        end

        -- 在门的旁边，或者已翻开的“非障碍”旁边
        if  g.type == GRID_TYPE_DOOR or g.type == GRID_TYPE_GARDEN or
            g.temp_refresh == true or  -- 是临时刷出的怪
            g.state == GRID_STATE_DISABLE or
            g.state ~= GRID_STATE_COVER and not block and
            (g:isPublicGrid() == false or g.activeState or g:isMonster() and g.monster:isDead()) then
            -- 有联通的临格
            ok = GRID_OPEN_OK;
            break;
        end
    end

    -- 暂时没有其他情况
    Profiler.funcEnd("canOpenGrid");
    return ok;
end

function addDelayAction()
    local toAction = delayAction;
    delayAction = {};

    for _, action in pairs(toAction) do
        addAction(action);
    end
end

-- 地牢探索行为
function addAction(action, delay)
    -- 不是游戏状态
    if DUNGEON_GAMING ~= state and
       not DungeonInstanceM.isInInstance() then
        return;
    end

    -- 验证客户端不需要
    if isVerifyClient() or StatusM.check(ME.user, "PLAYING_RECORD") then
        return;
    end

    -- 加入延时action
    if delay then
        table.insert(delayAction, action);

        return;
    end

    -- TODO: 这几个action后面没有其他的
    if "next_floor" == action.cmd or "leave_dungeon" == action.cmd or "pause_dungeon" == action.cmd then
        addDelayAction();
    end

    -- 第一个字节存放id，第二个字节pos，接着四个字节存放操作数据，最后两个字节存放操作累计次数，共8个字节
    local actionId = DungeonActionM.query(action.cmd, "id");
    local pos  = action.pos or 0;
    local data = action.data or 0;

    local num = #actionCache;

    -- 看下是否需要合并，如果前六个字节一样（id、pos、data）则需要合并次数
    if num > 0 then
        local recent = actionCache[num];

        -- 相同的操作
        if  Buffer.get8(recent, 1)  == actionId and
            Buffer.get16(recent, 2) == pos and
            Buffer.get32(recent, 4) == data then

            -- 修改次数
            local times = Buffer.get16(recent, 8) + (action["times"] or 1);
            Buffer.set16(recent, 8, times);

            addDelayAction();
            return;
        end
    end

    -- 新插入的一个操作
    -- 8个字节
    local buf = Buffer.create(8);

    -- id
    Buffer.set8(buf, 1, actionId);

    -- pos
    Buffer.set16(buf, 2, pos);

    -- data
    Buffer.set32(buf, 4, data);

    -- times
    Buffer.set16(buf, 8, (action["times"] or 1));

    table.insert(actionCache, buf);

    addDelayAction();
end

-- 访问建筑
function visitBuilding(pos)
    EventMgr.fire(event.VISIT_BUILDING, { ["pos"] = pos, });
    return true;
end

-- 开格子
function openGrid(pos)
    if not checkDungeonAction() then
        -- 经检测，不允许执行地牢行为
        return false;
    end

    local grid = getCurrentDungeon()[pos];

    -- 触发守卫失效
    local prop = PropM.combine(ME.user, "defense_lost", 1);
    PropM.trigger(ME.user, prop[1], prop[2], grid);

    -- 可以开，先改变状态
    grid:open();

    -- 如果格子被污染过
    if grid.polluted then
        -- 记一个debuff
        local polluted = grid.polluted;
        local prop = { polluted[1], 1, polluted[2], polluted[3], };
        local round = CombatM.getRound();
        if prop[4] > 0 then
            round = round + prop[4];
        else
            round = -1;
        end

        prop[4] = round;

        local status = polluted[5] or "poisoned";   -- 默认是中毒
        local condition = {
            ["prop"] = prop,
            ["end_round"] = round,
        };

        -- 需要计算一下免疫概率
        CombatStatusM.applyStatus(ME.user, status, condition);
    end

    DungeonLogM.addLog(string.format("[DungeonM]开启格子 %d", pos));

    -- 一个回合事件
    EventMgr.fire(event.COMBAT_ROUND, pos);
    return true;
end

-- 是否所有怪物都已经挂掉
function allMonsterIsDead()
    local isDead = true;
    local grids = getCurrentDungeon();
    for _, grid in pairs(grids) do
        -- 没死的怪物才需要处理
        if  grid:isMonster() and
            not grid.monster:isDead() then
            isDead = false;
            break;
        end
    end

    if DungeonTentM.findTentGrid() then
        -- 还有流浪者帐篷
        isDead = false;
    end

    if #MonsterBuildingM.findMonsterBuildingGrids() > 0 then
        -- 还有可以刷怪的建筑
        isDead = false;
    end

    if TransEliminateM.hasTransEliminate() then
        -- 还有转换元素
        isDead = false;
    end

    return isDead;
end

-- 打开门
function openDoor()
    if not isInDungeon() then
        trace("DungeonM", "当前不在迷宫中，无法打开门。");
        return false;
    end

    local door = CombatM.filterByClass(getCurrentDungeon(), GRID_TYPE_DOOR)[1];
    door:open();

    -- 标记门已开启
    door.opened = 1;

    return true;
end

-- 拾取物品
function pickUp(pos)
    Profiler.funcBegin("pickUp");

    if not checkDungeonAction() then
        -- 经检测，不允许执行地牢行为
        return false;
    end

    -- 格子
    local grids = getCurrentDungeon();
    local grid = grids[pos];
    local bonus = grid.bonus;

    -- 不是可以拾取的类型
    if  not grid.has_key
        and grid.type ~= GRID_TYPE_BEEHIVE
        and grid.type ~= GRID_TYPE_TRAP
        and grid.type ~= GRID_TYPE_MONSTER
        and grid.type ~= GRID_TYPE_BOSS
        and grid.type ~= GRID_TYPE_ITEM
        and grid.type ~= GRID_TYPE_BOX
        and grid.type ~= GRID_TYPE_SLIME
        and grid.type ~= GRID_TYPE_APPRENTICE
        and grid.type ~= GRID_TYPE_MAP
        and grid.type ~= GRID_TYPE_BOSS_BOX
        and grid.type ~= GRID_TYPE_ACTIVITY_BOSS_BOX
        and grid.type ~= GRID_TYPE_SUMMON
        and grid.type ~= GRID_TYPE_DIAMON_STATUE
        and grid.type ~= GRID_TYPE_GOLD_CAN then
        Profiler.funcEnd("pickUp");
        return false;
    end

    -- 冈布奥和学徒
    if (grid.type == GRID_TYPE_SLIME or grid.type == GRID_TYPE_APPRENTICE) and
        not allMonsterIsDead() then
        Profiler.funcEnd("pickUp");
        return getLocStr("not_pickup_slime"), true;
    end

    -- 如果是在元素领主副本，提示击杀领主才能领取
    if UnrealDungeonM.isInAltarRuins() and not allMonsterIsDead() then
        Profiler.funcEnd("pickUp");
        return getLocStr("need_kill_elem_lord"), true;
    end
    -- 一些提前显示出来的奖励
    if grid:isPublicGrid() and not grid.has_key and not allMonsterIsDead() and
        grid.type ~= GRID_TYPE_TRAP and -- 陷阱掉落
        ( not grid:isMonster() or grid.monster.dbase:query("flee") == 1) then
        Profiler.funcEnd("pickUp");
        return getLocStr("monster_not_clear"), true;
    end

    -- 如果是钥匙
    if grid.has_key then

        -- 如果怪物没死 就不能拾取钥匙
        if grid:isMonster() then
            if not grid.monster:isDead() then
                -- 怪物还没死
                trace("DungeonM", "怪物没死，不能拾取钥匙。");
                Profiler.funcEnd("pickUp");
                return false;
            end
        end

        local door = CombatM.filterByClass(grids, GRID_TYPE_DOOR)[1];

        -- 自动开启
        door:open();

        -- 标记门已开启
        door.opened = 1;

        grid.picked = true;

        grid.has_key = nil;

        -- 开门事件
        EventMgr.fire(event.OPEN_DOOR, pos);

        -- 增加行为
        addAction({ ["cmd"] = "pick_item", ["pos"] = pos, })

        -- 一个回合
        EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos, ["isDelay"] = true });
        Profiler.funcEnd("pickUp");
        return true, true;
    end

    -- 没有奖励就不能拾取
    if not bonus then
        trace("DungeonM", "格子%d没有奖励，不能拾取。", pos);
        Profiler.funcEnd("pickUp");
        return false;
    end

    -- 如果还未打开
    if not grid:isOpened() then
        trace("DungeonM", "格子还未打开，不能拾取物品。");
        Profiler.funcEnd("pickUp");
        return false;
    end

    -- 如果是怪物
    if grid:isMonster() then
        if not grid.monster:isDead() then
            -- 怪物还没死
            trace("DungeonM", "怪物没死，不能拾取物品。");
            Profiler.funcEnd("pickUp");
            return false;
        end
    end

    -- 奖励清除
    grid.bonus = nil;

    grid.picked = true;

    -- 将格子标记为已失效
    if grid.type ~= GRID_TYPE_TRAP then
        grid:changeState(GRID_STATE_DISABLE);
    end

    -- 探索点加成
    local count = bonus[3] or 0;
    local prop = PropM.combine(ME.user, "bonus", bonus[2]);
    count = PropM.apply(prop, count);

    -- 数量修改
    bonus[3] = count;

    -- 增加行为
    -- 这一句必须放在doBonus之前，以保证命令的先后顺序（doBonus中也会触发事件）
    addAction({ ["cmd"] = "pick_item", ["pos"] = pos, })

    local result;
    -- 如果是立即使用的道具，那么使用掉
    -- 应该在ProertyM里面做的，而且已经有现成的功能（配置auto_use便可）。by panyl
    if bonus[1] == 1 and type(bonus[2]) == "number" then
        local dbase = PropertyM.query(bonus[2], "dbase");
        if type(dbase) == "table" and dbase["immediately"] then
            local args = PropertyM.query(bonus[2], "args");
            bonus = args["bonus"];

            if type(bonus) == "string" and
                string.startWith(bonus, "formula_") then
                local prefix = "formula_";
                local formulaName = string.sub(bonus, string.len(prefix) + 1);
                bonus = FormulaM.invoke(formulaName, ME.user);
            end

            if type(bonus[1]) == "number" then
                -- 奖励
                result = BonusM.doBonus(bonus, "pick_item");
            elseif type(bonus[1]) == "table" then
                for _, bonus_info in ipairs(bonus) do
                    -- 奖励
                    result = BonusM.doBonus(bonus_info, "pick_item");
                end
            end
        else
            -- 奖励
            result = BonusM.doBonus(bonus, "pick_item");
        end
    else
        -- 奖励
        result = BonusM.doBonus(bonus, "pick_item");
    end

    -- ！！！！！！！！！！！！！！！！！！！！！！！！
    --  这里事件必须在奖励之后做。因为拾取第一颗龙珠时，会去抽取许愿选项，
    -- 需要抽取随机数，等等，如果不放在奖励之后就会导致先后顺序问题，而且拾取龙珠/抽取许愿还不在同一个回合中
    -- 一个回合事件
    EventMgr.fire(event.COMBAT_ROUND, { ["pos"] = pos, ["isDelay"] = true });

    -- 事件
    EventMgr.fire(event.PICK_UP_ITEM, {["bonus"] = bonus, ["pos"] = pos, ["newBonus"] = result, ["type"] = grid.type, ["class"] = grid.class, });

    -- 尝试完成成就：获得竞技场对手物品
    EventMgr.fire(event.GET_ARENA_ENEMY_ITEM, {["bonus"] = bonus, ["pos"] = pos, });

    Profiler.funcEnd("pickUp");
    return true, true;
end

-- 两个位置是否相邻
function isAdjoin(pos1, pos2)
    local coor1 = convertToCoor(pos1);
    local coor2 = convertToCoor(pos2);

    -- 距离为1
    return 1 == math.abs(coor1.x - coor2.x) + math.abs(coor1.y - coor2.y);
end

-- 两个位置是否互相在周围
function isAround(pos1, pos2)
    local coor1 = convertToCoor(pos1);
    local coor2 = convertToCoor(pos2);

    -- 相邻或对角
    return  1 == math.abs(coor1.x - coor2.x) + math.abs(coor1.y - coor2.y) or
            1 == math.abs(coor1.x - coor2.x) and 1 == math.abs(coor1.y - coor2.y);
end

-- 获取临格
function getAdjoinGrids(pos)
    return adjoinGridTable[pos];
end

-- 获取周围格
function getAroundGrids(pos)
    return aroundGridTable[pos];
end

-- 转化为坐标
function convertToCoor(pos)
    local y = math.modf((pos - 1) / DUNGEON_WIDTH) + 1;  -- 纵坐标
    local x = (pos - 1) % DUNGEON_WIDTH + 1;             -- 横坐标

    return { ["x"] = x, ["y"] = y, };
end

-- 转化为位置
function convertToPos(x, y)
    return (y - 1) * DUNGEON_WIDTH + (x - 1) + 1;
end

-- 获取随机种子，循环使用。这个接口不能随便乱用，一定要确保和服务器严格同步
function getRandSeed(desc)
    if not desc then
        error("desc不能为空")
    end

    if not dungeonContainer then
        return 0;
    end

    -- 根据游标
    local index = RandomFactoryM.fetchRandCursor(desc);
    local seeds = dungeonContainer.rand_seed;
    local num = #seeds;
    local seed = seeds[index % num + 1];

    -- 游标往下移动
    RandomFactoryM.useRandCursor(desc);

    local RandomType = RandomFactoryM.getRandomType(desc);

    local msg = string.format("[%s]获取随机数: %d, cursor: %d", RandomType, seed, index);

    DungeonLogM.addLog(msg);
    DungeonDebugM.addCursor(desc, RandomType, index);
    return seed;
end

-- 排序元素市局
function sortGrids(data)
    local arr = {};
    for pos, info in pairs(data) do
        local t = {
            ["class"] = table.keys(info)[1],
            ["element"] = table.values(info)[1],
            ["pos"] = pos,
        };

        table.insert(arr, t);
    end

    -- 排序一遍
    table.sort(arr, function(a, b)
        local weight1 = string.format("g%010d%010d%02d", a.class, a.element, a.pos);
        local weight2 = string.format("g%010d%010d%02d", b.class, b.element, b.pos);

        return weight1 < weight2;
    end);

    return arr;
end

-- 判断门是否已经开启
function isDoorOpened()
    if not isInDungeon() then
        return false;
    end

    local door = CombatM.filterByClass(getCurrentDungeon(), GRID_TYPE_DOOR)[1];
    if door == nil then
        return false;
    end

    return door.opened == 1;
end

-- 还原格子数据
function restoreGrid(grid, context)
    if not context or not context.context then
        -- 没有上下文
        return;
    end

    local info;
    if context.context.array then
        info = context.context:toTable()[grid:getPos()];
    else
        info = context.context[grid:getPos()];
    end

    if not info then
        return;
    end

    if info.state ~= GRID_STATE_COVER then
        -- 被翻开过，需要设置一下状态
        grid:changeState(info.state);
    elseif info.polluted then
        -- 被污染过的未翻开格子
       grid.polluted = info.polluted;
    end

    if info.track then
        local monster = grid.monster;
        monster.dbase:set("track", info.track);

        -- 如果有属性
        if info.prop then
            local prop = info.prop;
            monster.dbase:setTemp("prop", prop);

            -- 整理下
            PropM.arrangeProps(monster);
        end
    end

    if info.dragon then
        local monster = grid.monster;
        monster.dbase:set("dragon", info.dragon);

        -- 如果有属性
        if info.prop then
            local prop = info.prop;
            monster.dbase:setTemp("prop", prop);

            -- 整理下
            PropM.arrangeProps(monster);
        end
    end

    if 1 == info.show_key then
        local monster = grid.monster;
        monster.dbase:set("show_key", info.show_key);
    end

    if info.open_round then
        grid.openRound = info.open_round;
    end

    -- 门已开启的标识
    if info.opened then
        grid.opened = info.opened;
    end

    -- 已经失效的格子，不处理
    if info.state == GRID_STATE_DISABLE and not info.bonus or info.class == 0 then
        return;
    end

    -- 奖励
    if info.bonus then
        grid.bonus = info.bonus;
    end

    -- 未翻开过的格子就不用后续处理了
    -- if not grid:isOpened() then
    --     return;
    -- end

    -- 其他非公共特性交给子模块处理
    local rule = getModule(grid.class);

    if not rule or not rule.restoreGrid then
        -- 没有相应的子规则
        return;
    end

    -- 直接子规则处理
    rule.restoreGrid(grid, info);
end

-- 还原钥匙格子
function restoreKeyGrid()
    local keyPos = getCurrLayerKeyPos();
    if keyPos < 0 then
        return;
    end

    -- 如果门已经开了，移除格子上的钥匙
    if isDoorOpened() == true then
        local grid = getGridByPos(keyPos);
        if grid ~= nil then
            grid.has_key = nil;
        end
    end
end

-- 获取当前层钥匙所在的位置
function getCurrLayerKeyPos()
    if not isInDungeon() then
        return -1;
    end

    local layer = currentLayer();
    local ret = dungeonContainer.keyPos[layer];
    if type(ret) == 'number' then
        return ret;
    end

    return -1;
end

----------------------------
-- 获取地牢通过评价星级
-- @param dungeonId
function getEvaluateStar(dungeonId)
    local dungeonProgress = ME.user.dbase:query("dungeon_progress");
    if not dungeonProgress or not dungeonProgress[dungeonId] then
        return 0;
    end

    return dungeonProgress[dungeonId].star or 0;
end

--------------------
-- 收集上格子下文
-- @param pos  格子位置
function collectContext(pos)
    local grid = getGridByPos(pos);

    local ret = {
        ["state"] = grid.state,
        ["class"] = grid.class,
        ["element"] = grid.element or 0,
    };

    if grid.has_key == 1 or grid.hasKey == 1 then
        ret.has_key = 1;
        grid.hasKey = 1;
    end

    if grid.opened == 1 then
        ret.opened = 1;
    end

    -- 如果已经失效了，直接无视(宝箱格子需要特殊处理)
    if GRID_STATE_DISABLE == grid.state and
        grid.type ~= GRID_TYPE_BOX and
        grid.type ~= GRID_TYPE_SUMMON and
        not grid.bonus then
        return ret;
    end

    -- 如果是未翻开过的格子，目前只需要收集污染数据
    if not grid:isOpened() then
        local polluted = grid.polluted;
        if polluted and #polluted > 0 then
            ret["polluted"] = polluted;
        end

        -- 怪物的标记，暂时这样处理下
        local monster = grid.monster;
        if monster and monster:query("track") then
            ret["track"] = monster:query("track");

            ret["prop"] = monster.dbase:queryTemp("prop");
        end

        if monster and monster:query("dragon") then
            ret["dragon"] = monster:query("dragon");

            ret["prop"] = monster.dbase:queryTemp("prop");
        end

        if monster and monster:query("show_key") then
            ret["show_key"] = monster:query("show_key");
        end

        if grid.bonus then
            ret["bonus"] = grid.bonus;
        end

        -- return ret;
    elseif type(grid.openRound) == "number" then
        ret["open_round"] = grid.openRound;
    end

    -- 其他的交给子模块去收集
    local rule = getModule(grid.class);

    if not rule or not rule.collectContext then
        -- 没有相应的子规则
        return ret;
    end

    local otherContext = rule.collectContext(grid);

    -- 直接子规则处理
    return table.add(ret, otherContext);
end

-------------------------
-- 恢复地牢战斗
function restoreDungeonCombat(force)
    -- 是否暂离状态
    if not force and not isPause() then
         -- 当前不是暂离状态，无法恢复地牢
         return false;
     end

    local dungeon = dungeonContainer;
    if not dungeon then
        -- 获取不到地牢数据
        return false;
    end

    local layer = currentLayer();
    local curr = dungeon.combats[layer];
    local pos = 1;
    local context = ME.user.dbase:query("dungeon_context");
    while curr[pos] and context["context"][pos] do
        local info = context["context"][pos];

        if (info.state == GRID_STATE_DISABLE and not info.bonus) or info.class == 0 then
            -- 失效的给子，直接变为空格
            curr[pos] = { [0] = 0 };
        else
            curr[pos] = { [info["class"]] = info["element"] };
        end

        if 1 == info["hasKey"] then
            -- TODO 记录钥匙的位置
        end

        pos = pos + 1;
    end
    dungeon["combats"][1] = curr;

    -- 开始地牢
    startDungeon(context);

    return true;
end

-- 地牢是否暂停了
function isPause()
    local context = ME.user.dbase:query("dungeon_context");
    local pausing = context and #table.keys(context) > 0;
    return pausing or getState() == DUNGEON_PAUSING;
end

-- 是否有上下文
function hasContext()
    local context = ME.user.dbase:query("dungeon_context");
    local pausing = context and #table.keys(context) > 0;
    return pausing;
end

-- 开启所有格子
function openAllGrids()
    -- 获取所有格子
    local curDungeon = DungeonM.getCurrentDungeon();
    for _, grid in pairs(curDungeon) do
        if grid.state == GRID_STATE_COVER then
            grid:changeState(GRID_STATE_OPEN);
            local monster = grid.monster;
            if monster then
                -- 删除标记属性
                monster.dbase:deleteTemp("prop", "mark");

                -- 触发伤害
                local props = PropM.fetchProps(monster, "apper_damage");
                for _, prop in ipairs(props) do
                    PropM.trigger(monster, prop[1], prop[2], { ME.user, -1, });
                end
            end
        end
    end
end

-- 是否击杀了所有怪物
function isAllMonsterDead()
    -- 先取出所有格子
    local grids = DungeonM.getCurrentDungeon();

    for _, grid in pairs(grids) do
        -- 还有怪物没死
        if grid:isMonster() and
            not grid.monster:isDead() then
            return false;
        end
    end
    return true;
end

-- 判断指定怪物是否存活
function isMonsterAlive(baseClassId)
    -- 先取出所有格子
    local grids = DungeonM.getCurrentDungeon();

    local isAlive = false;
    for _, grid in pairs(grids) do
        repeat
            if grid.state ~= GRID_STATE_OPEN then
                break;
            end

            if not grid:isMonster() or
               grid.monster:isDead() then
                break;
            end

            if MonsterM.query(grid.monster.classId, "base_class_id") == baseClassId then
                isAlive = true;
            end
        until true
    end

    return isAlive;
end

-- 检查是否能执行地牢行为
function checkDungeonAction()
    if ME.user:getHp() <= 0 then
        trace("DungeonM", "玩家血量小于0，不允许执行地牢行为。");
        return false;
    end

    -- 正在自动打开格子
    if DungeonServiceM.getAutoOpeningFlag() then
        return false;
    end

    return true;
end

---------------------------
-- 将格子转换成怪物
-- @param pos 格子位置
function transGridToMonster(pos, element, desc, isHide)
    assert(type(pos) == 'number');
    assert(type(element) == 'number');

    if not isInDungeon() then
        -- 不在地牢中
        return false;
    end

    if not curDungeon[pos] then
        -- 指定格子位置不存在
        return false;
    end

    -- 旧的格子
    local oldGrid = curDungeon[pos];
    local gridType = oldGrid.type;

    -- 只有如下指定类型的格子、已失效的格子或隐形障碍格能直接转换成怪物
    local validTypes = { GRID_TYPE_EMPTY, GRID_TYPE_UNREAL_SPECIAL, };
    if table.indexOf(validTypes, gridType) == -1 and
       oldGrid.state ~= GRID_STATE_DISABLE and
       oldGrid.class ~= getInvisibleBlockClass() then
        trace("DungeonM", "类型为(%d)的格子不能转换成怪物", gridType);
        return false;
    end

    local elemInfo = queryElement(element);
    if not elemInfo then
        trace("DungeonM", "元素%d不存在", element);
        return false;
    end

    local class = elemInfo.class;
    local rule  = DungeonM.query(class, "rule");
    if rule ~= "monster" and rule ~= "mon_away_door2" and rule ~= "bishop_monster" then
        trace("DungeonM", "元素%d不是怪物。", element);
        return false;
    end

    -- 转化为坐标
    local coordinate = convertToCoor(pos);
    local x = coordinate.x;      -- 横坐标
    local y = coordinate.y;      -- 纵坐标

    -- 格子的属性
    local dbase = queryElement(element, "dbase");

    local entir = table.add(dbase, {});

    entir.layer = currentLayer();
    entir.pos = pos;

    entir.element = element;

    -- 标记这是临时刷新出来的怪
    entir.temp_refresh = true;

    entir = table.add(entir, FormulaM.invoke("CALC_DUNGEON_ELEMENT_ATTRIB",
            getDungeonId(), element, layer, pos));

    -- 奖励
    if oldGrid.bonus then
        entir.bonus = oldGrid.bonus;
    end
    if oldGrid.has_key and not oldGrid.picked then
        entir.has_key = oldGrid.has_key;
    end

    -- 创建一个格子
    curDungeon[pos] = Grid.new(class, x, y, GRID_STATE_OPEN, entir);
    curDungeon[pos].openRound = CombatM.getRound();
    curDungeon[pos].monster.isHide = isHide;

    -- 刷新下光环
    AssistMonsterM.checkMonsterAppear(pos);

    -- 抛出格子刷怪事件
    EventMgr.fire(event.GRID_REFRESH_MONSTER, {
        ["pos"] = pos,
        ["classId"] = curDungeon[pos].monster.classId,
        ["desc"] = desc,
        ["isHide"] = isHide,
     });

    return true;
end

-- 执行元素转换
function doElementTranform(pos)
    local grid = DungeonM.getGridByPos(pos);
    if not grid then
        trace("DungeonM", "无法执行元素转换，格子(%d)不存在。", pos);
        return false;
    end

    if grid.state == GRID_STATE_DISABLE then
        trace("DungeonM", "元素转换失败：格子(%d)已经失效。", pos);
        return false;
    end

    local element = grid.element;
    local elemDbase = DungeonM.queryElement(element, "dbase");
    local toElement = elemDbase.trans_element;
    if type(toElement) ~= "number" then
        trace("DungeonM", "元素转换失败：格子(%d)没有配置可转换的元素。", pos);
        return false;
    end

    local toClass = DungeonM.queryElement(toElement, "class");
    if type(toClass) ~= "number" then
        trace("DungeonM", "元素转换失败：元素(%d)不存在", toElement);
        return false;
    end

    local para = { ["class"] = toClass, ["element"] = toElement };

    -- 执行转换
    if not DungeonM.transformGrid(toClass, pos, GRID_STATE_OPEN, para) then
        trace("DungeonM", "格子转换失败。");
        return false;
    end

    trace("DungeonM", "格子(%d)成功转化成元素(%d)", pos, toElement);

    return true;
end

-- 转变一个格子
function transformGrid(class, pos, state, dbase)
    if not curDungeon or not curDungeon[pos] then
        trace("DungeonM", "格子(%d)不存在，无法转换格子。", pos);
        return false;
    end

    -- 转化为坐标
    local coordinate = convertToCoor(pos);
    local x = coordinate.x;      -- 横坐标
    local y = coordinate.y;      -- 纵坐标
    local layer = currentLayer();

    local para = table.add({
            ["layer"] = layer,
            ["pos"] = pos,
        }, dbase);

    local element = para["element"];
    local attrib = {};
    if element then
        attrib = FormulaM.invoke("CALC_DUNGEON_ELEMENT_ATTRIB",
            getDungeonId(), element, layer, pos);

        local elemDbase = queryElement(element, "dbase");
        if type(elemDbase) == "table" then
            attrib = table.add(attrib, elemDbase);
        end

        -- 如果奖励配置的是公式，需要调用公式重新计算
        local prefix = "formula_";
        if type(attrib) == "table" and type(attrib.bonus) == "string" and
            string.startWith(attrib.bonus, "formula_") then
            local formulaName = string.sub(attrib.bonus, string.len(prefix) + 1);
            attrib.bonus = FormulaM.invoke(formulaName, ME.user, attrib.bonusArg);
        end
    end

    para = table.add(para, attrib);

    -- 创建一个格子
    local grid = Grid.new(class, x, y, state, para);

    if para["open_round"] then
        grid.openRound = para["open_round"];
    end

    curDungeon[pos] = grid;

    -- 抛出转化格子事件
    EventMgr.fire(event.TRANSFORM_GRID, {
        ["pos"] = pos,
        ["isDelay"] = para.isDelay,
     });

    -- 刷新光环
    AssistMonsterM.checkMonsterAppear(pos);

    return grid;
end

-- 获取特殊道具出现次数信息
function getPropertyOccurs()
    return ME.user.dbase:query("property_occurs", {});
end

-- 统计特殊道具出现次数
function statPropertyOccurs(classId)
    local propertyOccurs = getPropertyOccurs();
    if not propertyOccurs[classId] then
        propertyOccurs[classId] = 1;
    else
        propertyOccurs[classId] = propertyOccurs[classId] + 1;
    end

    ME.user.dbase:set("property_occurs", propertyOccurs);
end

-- 地牢开始时是否默认开启所有格子
function isOpenAllGridsWhenStart()
    if DungeonInstanceM.isInInstance() then
        -- 副本中
        return true;
    end

    local dungeonId = DungeonM.getDungeonId();
    local layer     = DungeonM.currentLayer();
    if DungeonBossM.isBossLayer(dungeonId, layer) then
        -- 是boss层
        return true;
    end

    -- 夹层迷宫
    if UnrealDungeonM.isInUnreal() then
        return true;
    end

    -- 天空副本
    if SkyInstanceM.isInSkyInstance() and
        SkyInstanceM.needOpenAllGrids() then
        return true;
    end

    -- 活动副本Boss层
    if ActivityDungeonM.isInActivityBossLayer() then
        return true;
    end

    -- 觉醒试炼地牢
    if AwakeM.isAwakeDungeon(dungeonId) then
        return true;
    end

    return false;
end

-- 格子操作是否消耗回合数
function isCostRound(grid)
    local class = grid.class;

    local ret = query(class, "cost_round") == 1;

    if ret == true then
        trace("[DungeonM]", "class = %d的格子操作消耗了回合数。", class);
    end

    return ret;
end

-- 获取迷宫杂项字段
function getDungeonMixedField(path)
    local dungeonMixed = ME.user.dbase:query("dungeon_mixed");
    if not dungeonMixed then
        return nil;
    end

    if not path or path == "" then
        return dungeonMixed;
    end

    return dungeonMixed[path];
end

-- 设置迷宫杂项字段（目前迷宫杂项字段只能为:int或string类型）
function setDungeonMixedField(path, value)
    local dungeonMixed = ME.user.dbase:query("dungeon_mixed") or {};
    dungeonMixed[path] = value;
    ME.user.dbase:set("dungeon_mixed", dungeonMixed);
end

-- 是否在特殊层
function isInSpecailLayer()
    if DungeonBossM.isInBossLayer() then
        -- 是boss层
        return true;
    end

    if DungeonInstanceM.isInInstance() then
        -- 是副本
        return true;
    end

    if UnrealDungeonM.isInUnreal() then
        -- 是夹层迷宫
        return true;
    end

    return false;
end

-- 获取奖励描述
function getBonusDesc(desc, bonus)
    -- 获取奖励信息
    local bonusDesc = "";
    local seperator = "";

    if type(bonus[1]) == "table" then
        for i, v in pairs(bonus) do
            -- 拼接奖励信息
            if i ~= 1 then
                seperator = getLocStr("seperator");
            end

            if v[1] == 1 then
                -- 道具奖励
                bonusDesc = bonusDesc .. seperator .. string.format("%s+%d", ItemM.query(v[2], "name"), v[3]);
            elseif v[1] == 2 then
                -- 属性奖励
                bonusDesc = bonusDesc .. seperator .. string.format("%s+%d", FieldsM.getFieldName(v[2]), v[3]);
            end
        end
    elseif type(bonus[1]) == "number" then
        if bonus[1] == 1 then
            -- 道具奖励
            bonusDesc = string.format("%s+%d", ItemM.query(bonus[2], "name"), bonus[3]);
        elseif bonus[1] == 2 then
            -- 属性奖励
            bonusDesc = string.format("%s+%d", FieldsM.getFieldName(bonus[2]), bonus[3]);
        end
    end

    return desc .. "\n" .. bonusDesc;
end

-- 能否恢复属性
function canRecoverAttrib(who, attrib)
    if attrib ~= "hp" and attrib ~= "mp" and attrib ~= "limit_hp" and attrib ~= "limit_mp" then
        return true;
    end

    if #PropM.fetchProps(who, "blood_poisoned") > 0 then
        if who == ME.user then
            local msg = string.format("[DungeonM](%s)中了血毒，无法恢复属性(%s)", who:getName(), attrib);
            DungeonLogM.addLog(msg);
        else
            trace("DungeonM", "(%s)中了血毒，无法恢复属性(%s)", who:getName(), attrib)
        end

        -- 根据状态返回值
        local desc;
        if CombatStatusM.queryCondition(who, "dark_ceremony") then
            desc = "dark_ceremony";
        elseif CombatStatusM.queryCondition(who, "blood_poisoned") then
            desc = "blood_poisoned";
        end

        return desc;
    end

    return true;
end

-- 判断能否暂离
function canPauseDungeon()
    if not isInDungeon() then
        -- 不在迷宫中
        return false;
    end

    if SkyInstanceM.isInSkyInstance() then
        -- 在天空副本中
        return false;
    end

    -- 其他情形允许暂离
    return true;
end

-- 置灰暂离按钮的情况
function hidePauseDungeon()
    if DungeonInstanceM.isInInstance() or
        DungeonBossM.isInBossLayer() or
        UnrealDungeonM.isInUnreal() or
        -- ActivityDungeonM.isInActivityDungeon() or
        SkyInstanceM.isInSkyInstance() then
        -- 如下情形不能点击暂离
        -- 1.副本   2.boss战     3.夹层迷宫  4.活动迷宫  5.天空特殊副本
        return true;
    end
    return false;
end

-- 查找所有空位
-- @param allowBlock 是否包含障碍格子 是否包含未开启格（默认不包含）
function findEmptyGrids(allowBlock, allowCover)
    local ret = {};
    if not DungeonM.isInDungeon() then
        return {};
    end

    allowBlock = allowBlock or false;
    allowCover = allowCover or false;

    for pos = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(pos);
        repeat
            -- 未被翻开
            if not grid:isOpened() and not allowCover then
                break;
            end

            -- 有奖励
            if grid.bonus then
                break;
            end

            -- 有钥匙
            if grid.has_key then
                break;
            end

            if grid.type == GRID_TYPE_EMPTY or
               grid.state == GRID_STATE_DISABLE or
               (allowBlock and grid.type == GRID_TYPE_BLOCK) then
                -- 空格、失效的格子或者特定障碍格
                table.insert(ret, grid:getPos());
            end
        until true;
    end

    return ret;
end

-- 清除敌方怪物
function clearEnemyMonster()
    local grids = DungeonM.getCurrentDungeon();

    for _, grid in pairs(grids) do
        repeat
            if not grid:isMonster() or grid.monster:isDead() then
                -- 过滤非怪物，或者已经死亡的怪物
                break;
            end

            if grid.monster:isAudience() then
                -- 过滤观众
                break;
            end

            -- 执行死亡
            CombatM.doDamage(grid.monster, grid.monster, grid.monster:getHp(), 0);

        until true
    end
end

-- 查找已失效的格子（包含空格）
-- 如果存在多个，随机返回一个，否则返回nil
function findDisabledGrid()
    -- 先取出所有格子
    local grids = DungeonM.getCurrentDungeon() or {};

    local gridList = {};
    for _, grid in pairs(grids) do
        repeat
            -- 必须是已开启的空格或已失效的格子
            if grid.state == GRID_STATE_DISABLE or
                (grid.type == GRID_TYPE_EMPTY and grid.state == GRID_STATE_OPEN) then
                -- 不能刷在钥匙或者有奖励的格子上
                if grid.has_key ~= 1 and grid.bonus == nil then
                    table.insert(gridList, grid);
                end
            end
        until true
    end

    local gridCount = #gridList;
    if gridCount == 0 then
        return nil;
    end

    if gridCount == 1 then
        return gridList[1];
    end

    -- 存在多个，随机返回一个
    local randSeed = DungeonM.getRandSeed("find_disabled_grid");
    local index = randSeed % gridCount + 1;

    -- 根据位置排序
    table.sort(gridList, function(grid1, grid2) return grid1:getPos() < grid2:getPos(); end);

    return gridList[index];
end

-- 是否存在某种类型的格子
function isGridExist(gridType)
    local ret = false;
    for p = 1, GRID_SIZE do
        local grid = DungeonM.getGridByPos(p);

        -- 查找类型匹配且未失效的格子
        if grid.type == gridType and
           grid.state ~= GRID_STATE_DISABLE then
            ret = true;
            break;
        end
    end
    return ret;
end

-- 某种element的数量
function getElementNum(element)
    local num = 0;
    local grids = getCurrentDungeon();
    for _, grid in pairs(grids) do
        if grid.state ~= GRID_STATE_DISABLE
            and grid.element == element then
            num = num + 1;
        end
    end
    return num;
end
